﻿//-----------------------------------------------------------------------
// <copyright file="TiledMapResource.cs" company="WorldView Solutions Inc">
//     Copyright (c) WorldView Solutions Inc. This source is subject to the Microsoft Public License (Ms-PL). All other rights reserved.
// </copyright>
//-----------------------------------------------------------------------

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using System.Web;
using ESRI.ArcGIS.ADF.ArcGISServer;
using WorldViewSolutions.ArcGISServer.SOE;

namespace WorldViewSolutions.ArcGISServer.SOE.DataSources
{
    /// <summary>
    /// 
    /// </summary>
    /// <remarks></remarks>
	public class TiledMapResource : IMapDResource
	{
		#region [ Properties ]

        /// <summary>
        /// Gets or sets the URL format.
        /// </summary>
        /// <value>The URL format.</value>
        /// <remarks></remarks>
		public UrlFormat UrlFormat { get; set; }

        /// <summary>
        /// Gets or sets the name.
        /// </summary>
        /// <value>The name.</value>
        /// <remarks></remarks>
		public string Name { get; set; }

        /// <summary>
        /// Gets or sets the service urls.
        /// </summary>
        /// <value>The service urls.</value>
        /// <remarks></remarks>
		public string[] ServiceUrls { get; set; }


        /// <summary>
        /// Gets a value indicating whether this instance is cached.
        /// </summary>
        /// <remarks></remarks>
		public bool IsCached
		{
			get { return true; }
		}

        /// <summary>
        /// Gets the units.
        /// </summary>
        /// <remarks></remarks>
		public esriUnits Units
		{
			get { return esriUnits.esriUnknownUnits; }
		}

        /// <summary>
        /// Gets the tile cache info.
        /// </summary>
        /// <value>The tile cache info.</value>
        /// <remarks></remarks>
		public ESRI.ArcGIS.ADF.ArcGISServer.TileCacheInfo TileCacheInfo { get; set; }

        /// <summary>
        /// Gets or sets the tile image info.
        /// </summary>
        /// <value>The tile image info.</value>
        /// <remarks></remarks>
		public ESRI.ArcGIS.ADF.ArcGISServer.TileImageInfo TileImageInfo { get; set; }

		#endregion

		#region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="T:System.Object"/> class.
        /// </summary>
        /// <remarks></remarks>
		public TiledMapResource()
		{
            
		}


        /// <summary>
        /// Initializes a new instance of the <see cref="TiledMapResource"/> class.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="serviceUrls">The service urls.</param>
        /// <param name="configFile">The config file.</param>
        /// <param name="urlFormat">The URL format.</param>
        /// <remarks></remarks>
		public TiledMapResource(string name, string[] serviceUrls, string configFile, UrlFormat urlFormat) : this()
        {
			this.Name = name;
			this.ServiceUrls = serviceUrls;
			this.UrlFormat = urlFormat;
        }

        #endregion
        		
        #region Public Methods


        /// <summary>
        /// Draws the extent.
        /// </summary>
        /// <param name="widthInches">The width inches.</param>
        /// <param name="heightInches">The height inches.</param>
        /// <param name="outputDPI">The output DPI.</param>
        /// <param name="maxResourceDpi">The max resource dpi.</param>
        /// <param name="extentToDraw">The extent to draw.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public Bitmap DrawExtent(double widthInches, double heightInches, int outputDPI, int maxResourceDpi, EnvelopeN extentToDraw)
        {
            ArcGISLog.Write("BEGIN: DrawExtent()");
            ArcGISLog.Write("widthInches = " + widthInches.ToString() + ", heightInches = " + heightInches.ToString() + ", outputDPI = " + outputDPI.ToString() + ", maxResourceDpi = " + maxResourceDpi.ToString());
            ArcGISLog.Write("extentToDraw: XMin = " + extentToDraw.XMin + ", YMin = " + extentToDraw.YMin + ", XMax = " + extentToDraw.XMax + ", YMax = " + extentToDraw.YMax);
            
           
            // calculate the image size based on the requested dpi
            int outputWidthPixels = Convert.ToInt32(Math.Round(((double)widthInches * outputDPI), 0));
            int outputHeightPixels = Convert.ToInt32(Math.Round(((double)heightInches * outputDPI), 0));
            ArcGISLog.Write("outputWidthPixels = " + outputWidthPixels + ", outputHeightPixels = " + outputHeightPixels);

            // calculate the image size based on the cache's native dpi
            int imageWidthPixels = Convert.ToInt32(Math.Round(((double)widthInches * TileCacheInfo.DPI), 0));
            int imageHeightPixels = Convert.ToInt32(Math.Round(((double)heightInches * TileCacheInfo.DPI), 0));
            ArcGISLog.Write("imageWidthPixels = " + imageWidthPixels + ", imageHeightPixels = " + imageHeightPixels);

            // if the requested dpi is higher than the native cache's dpi, try to use a more detailed cahce level
            ArcGISLog.Write("outputDPI = " + outputDPI + ", TileCacheInfo.DPI = " + TileCacheInfo.DPI);
            if (outputDPI > TileCacheInfo.DPI)
            {
                // if the output dpi is greater than the max dpi, then use the max dpi. Most vector based caches (Roads) will have a max of 96. Using a more
                // detailed cache may results in additional data/layers being displayed than should be.
                if (outputDPI > maxResourceDpi)
                {
                    imageWidthPixels = Convert.ToInt32(Math.Round(((double)widthInches * maxResourceDpi), 0));
                    imageHeightPixels = Convert.ToInt32(Math.Round(((double)heightInches * maxResourceDpi), 0));
                }
                else
                {
                    // else use the output dpi, this is most commonly used for aerial photos
                    imageWidthPixels = outputWidthPixels;
                    imageHeightPixels = outputHeightPixels;
                }
                ArcGISLog.Write("imageWidthPixels = " + imageWidthPixels + ", imageHeightPixels = " + imageHeightPixels);
            }

            double allowableResolutionDelta = 20;

            double extentWidth = extentToDraw.XMax - extentToDraw.XMin;
            double resolution = extentWidth / imageWidthPixels;

            ArcGISLog.Write(string.Format("resolution = {0}, extentWidth = {1}", resolution, extentWidth));

            //our default will be the farthest in cache level
            LODInfo lodInfo = TileCacheInfo.LODInfos[TileCacheInfo.LODInfos.Length - 1];

            //bool resampleLowestScaleAvailable = true;
            for (int i = 0; i < TileCacheInfo.LODInfos.Length; i++)
            {
                LODInfo li = TileCacheInfo.LODInfos[i];

                double resolutionDifferenceAsPercentage = Math.Abs(((resolution - li.Resolution) / resolution) * 100);

                ArcGISLog.Write(string.Format("testing level {0}, scale {1}, resolution {2} against {3} -> {4}% difference", li.LevelID, li.Scale, li.Resolution, resolution, resolutionDifferenceAsPercentage));
               
                if (resolution >= li.Resolution || resolutionDifferenceAsPercentage <= allowableResolutionDelta)
                {
                    lodInfo = li;
                    break;
                }
            }

            ArcGISLog.Write(string.Format("Using Level: ID = {0}, scale = {1}, resolution = {2}", lodInfo.LevelID, lodInfo.Scale, lodInfo.Resolution));


            int requestWidth = -1;
            int requestHeight = -1;

            //if the resolution of the best available cache level doesn't match
            //the needed resolutions, we will have to resample the image appropriately
            if (true)
            //if (resolutionDelta > allowableResolutionDelta) 
            {
                requestWidth = imageWidthPixels;
                requestHeight = imageHeightPixels;

                try
                {
                    imageWidthPixels = Convert.ToInt32(requestWidth / (lodInfo.Resolution / resolution));
                }
                catch
                {
                    throw new Exception("Invalid map width: " + Convert.ToInt32(requestWidth / (lodInfo.Resolution / resolution)).ToString()
                        + ", requestWidth = " + requestWidth.ToString()
                        + ", lodInfo.Resolution = " + lodInfo.Resolution.ToString()
                        + ", resolution = " + resolution.ToString()
                        + ", extentToDraw.Width = " + extentWidth.ToString());
                }

                try
                {
                    imageHeightPixels = Convert.ToInt32(requestHeight / (lodInfo.Resolution / resolution));
                }
                catch
                {
                    throw new Exception("Invalid map height: " + Convert.ToInt32(requestHeight / (lodInfo.Resolution / resolution)).ToString());
                }

                ArcGISLog.Write(string.Format("Calculated width and hieght based on Level: width = {0}, height = {1}", imageWidthPixels, imageHeightPixels));
            }

            

            Bitmap mapImage = new Bitmap(imageWidthPixels, imageHeightPixels);
            
            mapImage.SetResolution(TileCacheInfo.DPI, TileCacheInfo.DPI);
            System.Drawing.Graphics mapGraphic = System.Drawing.Graphics.FromImage(mapImage);
            mapGraphic.Clear(System.Drawing.Color.White);

            double tileExtentWidth = TileCacheInfo.TileCols * lodInfo.Resolution;
            double tileExtentHeight = TileCacheInfo.TileRows * lodInfo.Resolution;
            PointN origin = (PointN)TileCacheInfo.TileOrigin;

            //these are incremented in the looping code to call the correct tiles            
            int rowCounter = 0;
            int colCounter = 0;
            int requestCounter = 0;

            //ArcGISLog.Write("TileCacheInfo.TileCols = " + TileCacheInfo.TileCols);
            //ArcGISLog.Write("TileCacheInfo.TileRows = " + TileCacheInfo.TileRows);
            //ArcGISLog.Write("lodInfo.Resolution = " + lodInfo.Resolution);
            //ArcGISLog.Write("origin.Y = " + origin.Y);
            //ArcGISLog.Write("tileExtentHeight = " + tileExtentHeight);

            //calculate first row and col to pull
            int firstRowToFetch = Convert.ToInt32(Math.Floor((origin.Y - extentToDraw.YMax) / tileExtentHeight));
            int firstColumnToFetch = Convert.ToInt32(Math.Floor((extentToDraw.XMin - origin.X) / tileExtentWidth));

            //ArcGISLog.Write("firstRowToFetch = " + firstRowToFetch);
            //ArcGISLog.Write("firstColumnToFetch = " + firstColumnToFetch);
            //ArcGISLog.Write("");

            double xMin = 0;
            double yMax = 0;

            //TODO: There could be more than one url that provides tiles. We could load balance by using each one.
            string serviceUrl = null;
            if (this.ServiceUrls != null && this.ServiceUrls.Length > 0)
            {
                serviceUrl = this.ServiceUrls[0];
            }

            string imageType = TileImageInfo.CacheTileFormat;
            //loop through cache rows
            do
            {
                //loop though cache columns
                do
                {
                    requestCounter++;

                    int row = firstRowToFetch + rowCounter;
                    int col = firstColumnToFetch + colCounter;

                    xMin = (col * tileExtentWidth) + origin.X;
                    yMax = origin.Y - (row * tileExtentHeight);       
                    
                    // calculate top left position of tile
                    int left = Convert.ToInt32(Math.Round((xMin - extentToDraw.XMin) / (tileExtentWidth / TileCacheInfo.TileCols)));
                    int top = Convert.ToInt32(imageHeightPixels - Math.Round((yMax - extentToDraw.YMin) / lodInfo.Resolution));
                    
                    using (Bitmap tile = GetTile(lodInfo.LevelID, col, row, serviceUrl, imageType))
                    {
                        if (tile != null)
                        {
                            //ArcGISLog.Write("Adding tile to Bitmap at position: top = " + top.ToString() + ", left = " + left.ToString());
                            mapGraphic.DrawImage(tile, left, top);
                        }
                    }

                    colCounter++;
                    // if (requestCounter > 25) break; // dont' allow more than 25 tiles to be pulled per column-row.
                } while (xMin + tileExtentWidth <= extentToDraw.XMax);
                // if (requestCounter > 25) break; // dont' allow more than 25 tiles to be pulled per column-row.
                rowCounter++;
                colCounter = 0;
            } while (yMax - tileExtentHeight > extentToDraw.YMin);

            return mapImage;

            /*
            if (imageWidthPixels != outputWidthPixels)
            {
                // Encoder parameter for image quality. If the dpi is 96 use a standard compression of 75, else use 90 (less compression) which will produce a higher quality image.
                int jpgQuality = mapImage.HorizontalResolution > 96f ? 90 : 75;
                EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, jpgQuality);
                ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
                EncoderParameters encoderParams = new EncoderParameters(1);
                encoderParams.Param[0] = qualityParam;

                ArcGISLog.Write(String.Format("resampling image from {0} x {1}  to  {2} x {3}", imageWidthPixels, imageHeightPixels, outputWidthPixels, outputHeightPixels));
                System.Drawing.Bitmap resampledImage = new Bitmap(outputWidthPixels, outputHeightPixels);
                System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(resampledImage);
                g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                g.DrawImage(mapImage, 0, 0, outputWidthPixels, outputHeightPixels);
                g.Dispose();
                mapImage = resampledImage;
            }

            mapImage.Tag = "DisconnectedMapResource " + this.Name == null ? "" : this.Name;
            ArcGISLog.Write("END: DrawExtent()");

            return mapImage;
            */
        }

        /// <summary>
        /// Returns the image codec with the given mime type
        /// </summary>
        /// <param name="mimeType">Type of the MIME.</param>
        /// <returns></returns>
        /// <remarks></remarks>
		private static ImageCodecInfo GetEncoderInfo(string mimeType)
		{
			// Get image codecs for all image formats 
			ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();

			// Find the correct image codec 
            for (int i = 0; i < codecs.Length; i++)
            {
                if (codecs[i].MimeType == mimeType)
                {
                    return codecs[i];
                }
            }

			return null;
		}

        /// <summary>
        /// Gets the tile URL.
        /// </summary>
        /// <param name="level">The level.</param>
        /// <param name="column">The column.</param>
        /// <param name="row">The row.</param>
        /// <param name="vdir">The vdir.</param>
        /// <param name="imageType">Type of the image.</param>
        /// <returns></returns>
        /// <remarks></remarks>
		public virtual string GetTileUrl(int level, int column, int row, string vdir, string imageType)
		{
			if (UrlFormat == UrlFormat.Disconnected)
			{
				string sLevel = "L" + Pad(level.ToString(), "0", 2, true);
				string sRow = "R" + Pad(row.ToString("X"), "0", 8, true);
				string sColumn = "C" + Pad(column.ToString("X"), "0", 8, true);
				return vdir + "/" + sLevel + "/" + sRow + "/" + sColumn + "." + imageType;
			}
			else
			{
				return vdir + "/" + level + "/" + row + "/" + column + "." + imageType;
			}
		}

        /// <summary>
        /// Pads the specified to pad.
        /// </summary>
        /// <param name="toPad">To pad.</param>
        /// <param name="padding">The padding.</param>
        /// <param name="totalLength">The total length.</param>
        /// <param name="padLeft">if set to <c>true</c> [pad left].</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private string Pad(string toPad, string padding, int totalLength, bool padLeft)
        {
            if (toPad.Length < totalLength)
            {
                if (padLeft)
                {
                    toPad = padding + toPad;
                }
                else
                {
                    toPad = toPad + padding;
                }
            }

            if (toPad.Length >= totalLength)
            {
                return toPad;
            }

            return Pad(toPad, padding, totalLength, padLeft);
        }

        /// <summary>
        /// Gets the tile.
        /// </summary>
        /// <param name="level">The level.</param>
        /// <param name="column">The column.</param>
        /// <param name="row">The row.</param>
        /// <param name="vdir">The vdir.</param>
        /// <param name="imageType">Type of the image.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public virtual Bitmap GetTile(int level, int column, int row, string vdir, string imageType)
        {
            try
            {
                string url = GetTileUrl(level, column, row, vdir, imageType);
                ArcGISLog.Write("BEGIN: Requesting tile:  " + url);

                System.Net.HttpWebRequest request = ProxiedWebRequest.Create(url);


                System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)request.GetResponse();

                if (response.StatusCode == HttpStatusCode.OK)
                {
                    using (System.IO.Stream responseStream = response.GetResponseStream())
                    {
                        Bitmap tile = new Bitmap(responseStream);
                        tile.SetResolution(TileCacheInfo.DPI, TileCacheInfo.DPI);
                        ArcGISLog.Write("END: Requesting tile");
                        return tile;
                    }
                }
                ArcGISLog.Write("END: Requesting tile:  Tile Failed - status code " + response.StatusCode.ToString());
            }
            catch (Exception ex)
            {
                ArcGISLog.WriteError("END: Requesting tile:  ERROR - " + ex.Message);
            }

            return null;
        }
        #endregion

    }
}